﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Numerics;
using ArgusLib.Threading;
using ArgusLib.Collections;
using ArgusLib.Xml;
using Thread = System.Threading.Thread;

namespace ArgusLib.Fractals.OrbitFractals
{
	public abstract class FractalComplex : IOrbitFractal
	{
		#region Private Fields
		const int listSize = 100;
		const float freeMemoryFactor = 0.8f;
		//const int numberOfThreads = 8;

		ThreadState threadState;
		Thread thread;
		#endregion

		#region Properties
		private IDataSetProvider dataSetProvider;
		public IDataSetProvider DataSetProvider
		{
			get { return this.dataSetProvider; }
			set
			{
				if (value == null)
					throw new ArgumentNullException();
				if (this.SupportsDataSetProvider(value) == false)
					throw new ArgumentException("Implementation of IDataSetProvider is not supported.");
				this.dataSetProvider = value;
			}
		}

		public bool IsCalculating { get { return this.thread != null; } }

		DataSet<Complex> dataSet { get; set; }
		public IDataSet DataSet { get { return this.dataSet; } private set { this.dataSet = (DataSet<Complex>)value; } }
		IParameterIterations Parameter { get { return this.Parameter_get(); } set { this.Parameter_set(value); } }
		IParameterIterations IOrbitFractal.Parameter { get { return this.Parameter; } set { this.Parameter = value; } }
		protected abstract IParameterIterations Parameter_get();
		protected virtual void Parameter_set(IParameter value)
		{
			if (value == null)
				throw new ArgumentNullException();
			if (!(value is IParameterIterations))
				throw new ArgumentException();
		}
		IParameter IFractal.Parameter { get { return this.Parameter; } set { this.Parameter = value as IParameterIterations; } }

		public string DataSetDumpFile { get; set; }
		#endregion

		#region Constructors
		public FractalComplex(Parameter parameter)
		{
			if (parameter == null)
				throw new ArgumentNullException();
			this.Parameter = parameter;
			this.DataSetProvider = new DataSetComplexProvider();
		}
		#endregion

		#region Public Methdos
		public bool SupportsDataSetProvider(IDataSetProvider dataSetProvider)
		{
			if (dataSetProvider.DataSetType.Is(typeof(DataSet<Complex>)) == true)
				return true;
			else
				return false;
		}

		public void StartCalculation()
		{
			if (this.IsCalculating == true)
				throw new Exception("There already is a calculation running.");
			this.thread = new Thread(new System.Threading.ThreadStart(this.Calculate));
			this.threadState = new ThreadState();
			this.thread.Start();
		}

		public void AbortCalculation(bool join)
		{
			this.threadState.RequestCancel();
			if (join == true)
				this.thread.Join();
		}

		public void Calculate()
		{
			if (this.threadState == null)
				this.threadState = new ThreadState();

			DateTime start = DateTime.Now;

			IDataSet dataSetBackUp = this.DataSet;
			this.DataSet = this.DataSetProvider.GetDataSet();

			System.Diagnostics.PerformanceCounter pCounter = null;
			float memoryUsage = 0;
			float memoryAvailable = 0;
			if (System.Diagnostics.PerformanceCounterCategory.CounterExists("Available MBytes", "Memory") == true)
			{
				pCounter = new System.Diagnostics.PerformanceCounter("Memory", "Available MBytes");
				memoryAvailable = pCounter.NextValue();
				memoryUsage = this.DataSet.GetMemoryUsage(this.Parameter);
				if (memoryAvailable*freeMemoryFactor < memoryUsage)
				{
					WarningReceivedEventArgs e = new WarningReceivedEventArgs((int)WarningReceivedEventArgs.Warnings.MemoryShortage);
					this.OnWarningReceived(e);
					if (e.Abort == true)
						this.threadState.RequestCancel();
				}
			}

			if (this.threadState.CancelRequested == false)
			{
				this.DataSet.DumpFile = this.DataSetDumpFile;
				this.DataSet.Initialize(this.Parameter);

				if (pCounter != null)
				{
					float available = pCounter.NextValue();
					float usage = memoryAvailable - available;
					if (memoryAvailable * freeMemoryFactor < usage)
					{
						WarningReceivedEventArgs e = new WarningReceivedEventArgs((int)WarningReceivedEventArgs.Warnings.MemoryShortage);
						this.OnWarningReceived(e);
						if (e.Abort == true)
							this.threadState.RequestCancel();
					}
					else
					{
						usage = this.DataSet.GetMemoryUsage();
						if (available * freeMemoryFactor < usage)
						{
							WarningReceivedEventArgs e = new WarningReceivedEventArgs((int)WarningReceivedEventArgs.Warnings.MemoryShortage);
							this.OnWarningReceived(e);
							if (e.Abort == true)
								this.threadState.RequestCancel();
						}
					}
				}
			}

			if (this.threadState.CancelRequested == false)
			{
				IEnumerable<Parallelization.TaskBody> tasks = this.GetStartTasks();

				Parallelization.ProcessTasksProgressHandler progressHandler = new Parallelization.ProcessTasksProgressHandler((int tasksProcessed, int tasksToProcess) =>
					{
						double progress = (double)tasksProcessed / (tasksProcessed + tasksToProcess);
						this.OnCalculationProgressChanged(new CalculationProgressChangedEventArgs(progress, DateTime.Now - start, tasksProcessed, tasksToProcess));
					});

				int threadCount = this.Parameter.ThreadCount;
				if (threadCount < 1)
					threadCount = System.Environment.ProcessorCount;
				Parallelization.ProcessTasks(tasks, threadCount, this.threadState, progressHandler);
			}

			if (this.threadState.CancelRequested == true)
			{
				this.DataSet = dataSetBackUp;
				this.OnCalculationAborted(new CalculationAbortedEventArgs());
			}
			else
			{
				this.DataSet.PostProcess();
				this.OnCalculationFinished(new CalculationFinishedEventArgs(DateTime.Now - start));
			}
		}

		const string XmlParameter = "Parameter";
		const string XmlDataSetProvider = "DataSetProvider";

		void ISerializable.Serialize(Serializer serializer)
		{
			serializer.Serialize(XmlParameter, this.Parameter);
			serializer.Serialize(XmlDataSetProvider, this.DataSetProvider);
		}

		void ISerializable.Deserialize(Deserializer deserializer)
		{
			ListPair<string, object> objs = deserializer.DeserializeAll();
			int i = objs.List1.IndexOf(XmlParameter);
			if (i > -1)
				this.Parameter = objs.List2[i] as Parameter;
			i = objs.List1.IndexOf(XmlDataSetProvider);
			if (i > -1)
				this.DataSetProvider = objs.List2[i] as IDataSetProvider;
		}
		#endregion

		#region Non-Public Methods
		private int GetIndex(int x, int y)
		{
			return y * this.Parameter.PixelWidth + x;
		}

		private void GetCoords(int i, out int x, out int y)
		{
			x = i % this.Parameter.PixelWidth;
			y = i / this.Parameter.PixelWidth;
		}

		protected virtual IEnumerable<Parallelization.TaskBody> GetStartTasks()
		{
			List<Parallelization.TaskBody> tasks = new List<Parallelization.TaskBody>();
			List<int> coords = new List<int>(2*listSize);
			for (int x = 0; x < this.Parameter.PixelWidth; x = x+2)
			{
				coords.Add(this.GetIndex(x, 0));
				coords.Add(this.GetIndex(x, this.Parameter.PixelHeight - 1));
				if (coords.Count > listSize)
				{
					tasks.Add(this.GetTask(coords));
					coords = new List<int>(2 * listSize);
				}
			}

			for (int y = 0; y < this.Parameter.PixelHeight; y = y+2)
			{
				coords.Add(this.GetIndex(0, y));
				coords.Add(this.GetIndex(this.Parameter.PixelWidth - 1, y));
				if (coords.Count > listSize)
				{
					tasks.Add(this.GetTask(coords));
					coords = new List<int>(2*listSize);
				}
			}
			if (coords.Count > 0)
				tasks.Add(this.GetTask(coords));

			return tasks;
		}

		private Parallelization.TaskBody GetTask(List<int> Coords)
		{
			return new Parallelization.TaskBody((state) =>
				{
					List<Parallelization.TaskBody> newTasks = new List<Parallelization.TaskBody>();
					List<int> newCoords = new List<int>(2*listSize);
					for (int i = 0; i < Coords.Count; i++)
					{
						if (state.CancelRequested == true)
							return null;

						int x, y;
						this.GetCoords(Coords[i], out x, out y);

						if (this.DataSet.IsNull[x,y] == false)
							continue;

						this.IteratePoint(x, y);
						this.DataSet.IsFinished[x, y] = true;

						if (state.CancelRequested == true)
							return null;

						if (this.dataSet.Iterations[x, y] < this.Parameter.Iterations) // Not in the set
						{
							if (x > 0)
							{
								if (this.DataSet.IsNull[x - 1, y] == true)
								{
									int coord = this.GetIndex(x - 1, y);
									if (Coords.Contains(coord) == false && newCoords.Contains(coord) == false)
										newCoords.Add(coord);
								}
							}
							if (y > 0)
							{
								if (this.DataSet.IsNull[x, y - 1] == true)
								{
									int coord = this.GetIndex(x, y - 1);
									if (Coords.Contains(coord) == false && newCoords.Contains(coord) == false)
										newCoords.Add(coord);
								}
							}
							if (x + 1 < this.DataSet.DimensionX)
							{
								if (this.DataSet.IsNull[x + 1, y] == true)
								{
									int coord = this.GetIndex(x + 1, y);
									if (Coords.Contains(coord) == false && newCoords.Contains(coord) == false)
										newCoords.Add(coord);
								}
							}
							if (y + 1 < this.DataSet.DimensionY)
							{
								if (this.DataSet.IsNull[x, y + 1] == true)
								{
									int coord = this.GetIndex(x, y + 1);
									if (Coords.Contains(coord) == false && newCoords.Contains(coord) == false)
										newCoords.Add(coord);
								}
							}

							if (newCoords.Count > listSize)
							{
								Parallelization.TaskBody newTask = this.GetTask(newCoords);
								newTasks.Add(newTask);
								newCoords = new List<int>(2*listSize);
							}
						}
					}
					if (newCoords.Count > 0)
					{
						Parallelization.TaskBody newTask = this.GetTask(newCoords);
						newTasks.Add(newTask);
						newCoords = null;
					}
					return newTasks;
				});
		}

		protected abstract void IteratePoint(int x, int y);
		#endregion

		#region Events
		/// <summary>
		/// Data[0]: int: Number of already processed tasks.
		/// Data[1]: int: Number of tasks that still need to be processed.
		/// </summary>
		public event CalculationProgressChangedEventHandler CalculationProgressChanged;
		public event CalculationAbortedEventHandler CalculationAborted;
		public event CalculationFinishedEventHandler CalculationFinished;
		public event WarningReceivedEventHandler WarningReceived;

		protected virtual void OnCalculationProgressChanged(CalculationProgressChangedEventArgs e)
		{
			if (this.CalculationProgressChanged != null)
				this.CalculationProgressChanged(this, e);
		}

		protected virtual void OnCalculationAborted(CalculationAbortedEventArgs e)
		{
			this.thread = null;
			if (this.CalculationAborted != null)
				this.CalculationAborted(this, e);
		}

		protected virtual void OnCalculationFinished(CalculationFinishedEventArgs e)
		{
			this.thread = null;
			if (this.CalculationFinished != null)
				this.CalculationFinished(this, e);
		}

		protected virtual void OnWarningReceived(WarningReceivedEventArgs e)
		{
			if (this.WarningReceived != null)
				this.WarningReceived(this, e);
		}
		#endregion
	}
}
